package com.xyazm.inventory.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import com.xyazm.common.constants.CoreConstant;
import com.xyazm.common.constants.RedisLockKeyConstants;
import com.xyazm.common.logger.LoggerFormat;
import com.xyazm.common.redis.RedisCache;
import com.xyazm.common.redis.RedisLock;
import com.xyazm.common.utils.ParamCheckUtil;
import com.xyazm.inventory.cache.CacheSupport;
import com.xyazm.inventory.dao.ProductStockDAO;
import com.xyazm.inventory.dao.ProductStockLogDAO;
import com.xyazm.inventory.domain.dto.DeductStockDTO;
import com.xyazm.inventory.domain.entity.ProductStockDO;
import com.xyazm.inventory.domain.entity.ProductStockLogDO;
import com.xyazm.inventory.domain.param.*;
import com.xyazm.inventory.enums.StockLogStatusEnum;
import com.xyazm.inventory.exception.InventoryBizException;
import com.xyazm.inventory.exception.InventoryErrorCodeEnum;
import com.xyazm.inventory.processor.*;
import com.xyazm.inventory.service.IInventoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.text.MessageFormat;
import java.util.List;
import java.util.Map;

@Slf4j
@Service
public class InventoryServiceImpl implements IInventoryService {

    @Autowired
    private ProductStockDAO productStockDAO;
    @Autowired
    private ProductStockLogDAO productStockLogDAO;

    @Autowired
    private AddProductStockProcessor addProductStockProcessor;
    @Autowired
    private ReleaseProductStockProcessor releaseProductStockProcessor;
    @Autowired
    private DeductProductStockProcessor deductProductStockProcessor;
    @Autowired
    private ModifyProductStockProcessor modifyProductStockProcessor;
    @Autowired
    private SyncStockToCacheProcessor syncStockToCacheProcessor;


    @Autowired
    private RedisLock redisLock;
    @Autowired
    private RedisCache redisCache;


    @Override
    public void deductProductStock(DeductProductParam param) {

        log.info(LoggerFormat.build()
                .remark("deductProductStock->request")
                .data("request", param)
                .finish());

        // 检查入参
        checkLockProductStockParam(param);
        String orderId = param.getOrderId();
        // 订单条目信息
        List<DeductProductParam.OrderItemParam> orderItemParamList = param.getOrderItemParamList();
        for (DeductProductParam.OrderItemParam orderItemParam : orderItemParamList) {
            String skuCode = orderItemParam.getSkuCode();
            // 扣减商品库存锁（保证mysql+redis库存扣减的原子性）
            String lockKey = RedisLockKeyConstants.DEDUCT_PRODUCT_STOCK_KEY + skuCode;
            try {
                // 1、添加redis锁扣库存锁
                // (1) 防同一笔订单重复扣减
                // (2) 重量级锁，保证mysql+redis扣库存的原子性，同一时间只能有一个订单来扣，
                // 需要锁查询+扣库存
                // 获取不到锁，阻塞等待,默认等待3秒
                boolean locked = redisLock.tryLock(lockKey, CoreConstant.DEFAULT_WAIT_SECONDS);
                if (!locked) {
                    log.error(LoggerFormat.build()
                            .remark("无法获取扣减库存锁")
                            .data("orderId", orderId)
                            .data("skuCode", skuCode)
                            .finish());
                    throw new InventoryBizException(InventoryErrorCodeEnum.DEDUCT_PRODUCT_SKU_STOCK_CANNOT_ACQUIRE);
                }

                // 2、查询mysql库存数据
                ProductStockDO productStockDO = productStockDAO.getBySkuCode(skuCode);
                log.info(LoggerFormat.build()
                        .remark("查询mysql库存数据")
                        .data("orderId", orderId)
                        .data("productStockDO", productStockDO)
                        .finish());
                if (productStockDO == null) {
                    log.error(LoggerFormat.build()
                            .remark("商品库存记录不存在")
                            .data("skuCode", skuCode)
                            .finish());
                    throw new InventoryBizException(InventoryErrorCodeEnum.PRODUCT_SKU_STOCK_NOT_FOUND_ERROR);
                }


                // 3、查询redis库存数据
                String productStockKey = CacheSupport.buildProductStockKey(skuCode);
                Map<String, String> productStockValue = redisCache.hGetAll(productStockKey);
                if (productStockValue.isEmpty()) {
                    // 如果查询不到redis库存数据，将mysql库存数据放入redis，以mysql的数据为准
                    addProductStockProcessor.addStockToRedis(productStockDO);
                }


                // 4、查询库存扣减日志
                ProductStockLogDO productStockLog = productStockLogDAO.getLog(orderId, skuCode);
                if (null != productStockLog) {
                    log.info("已扣减过，扣减库存日志已存在,orderId={},skuCode={}", orderId, skuCode);
                    return;
                }

                Integer saleQuantity = orderItemParam.getSaleQuantity();

                // 5、执行执库存扣减
                DeductStockDTO deductStock = new DeductStockDTO(orderId, skuCode, saleQuantity, productStockDO);
                deductProductStockProcessor.doDeduct(deductStock);

            } finally {
                redisLock.unlock(lockKey);
            }
        }
        log.info(LoggerFormat.build()
                .remark("deductProductStock->response")
                .finish());
    }

    @Override
    public void releaseProductStock(ReleaseProductStockParam param) {
        String orderId = param.getOrderId();
        log.info("释放库存,orderId:{}",orderId);
        // 检查释放商品库存入参
        checkReleaseProductStockParam(param);
        List<ReleaseProductStockParam.OrderItemParam> orderItemParamList = param.getOrderItemParamList();

        List<String> redisKeyList = Lists.newArrayList();
        //  分布式锁
        for (ReleaseProductStockParam.OrderItemParam orderItemParam : orderItemParamList) {
            //  lockKey: #MODIFY_PRODUCT_STOCK_KEY: skuCode
            String lockKey = RedisLockKeyConstants.MODIFY_PRODUCT_STOCK_KEY + orderItemParam.getSkuCode();
            redisKeyList.add(lockKey);
        }
        boolean lock = redisLock.multiLock(redisKeyList);
        if (!lock) {
            throw new InventoryBizException(InventoryErrorCodeEnum.RELEASE_PRODUCT_SKU_STOCK_ERROR);
        }

        for (ReleaseProductStockParam.OrderItemParam orderItem : orderItemParamList) {
            String skuCode = orderItem.getSkuCode();
            String lockKey = RedisLockKeyConstants.RELEASE_PRODUCT_STOCK_KEY + skuCode;
            // 获取不到锁，等待3s
            Boolean locked = redisLock.tryLock(lockKey, CoreConstant.DEFAULT_WAIT_SECONDS);
            if (!locked) {
                log.error("无法获取释放库存锁,orderId={},skuCode={}", orderId, skuCode);
                throw new InventoryBizException(InventoryErrorCodeEnum.RELEASE_PRODUCT_SKU_STOCK_LOCK_CANNOT_ACQUIRE);
            }
            try {
                // 2.查询mysql库存数据
                ProductStockDO productStock = productStockDAO.getBySkuCode(skuCode);
                if (productStock == null) {
                    throw new InventoryBizException(InventoryErrorCodeEnum.PRODUCT_SKU_STOCK_NOT_FOUND_ERROR);
                }
                // 3.查询redis库存数据
                String productStockKey = CacheSupport.buildProductStockKey(skuCode);
                Map<String, String> productStockValue = redisCache.hGetAll(productStockKey);
                if (productStockValue.isEmpty()) {
                    // 如果查询不到redis库存数据，将mysql库存数据放入redis，以mysql的数据为准
                    addProductStockProcessor.addStockToRedis(productStock);
                }
                // 订单销售数量
                Integer saleQuantity = orderItem.getSaleQuantity();

                // 4.校验是否释放过库存
                ProductStockLogDO productStockLog = productStockLogDAO.getLog(orderId, skuCode);
                if (productStockLog == null) {
                    log.info("空回滚，库存根本就没扣减过,orderId={},skuCode={}", orderId, skuCode);
                    return;
                }
                if (productStockLog.getStatus().equals(StockLogStatusEnum.RELEASED.getCode())) {
                    log.info("已释放过库存,orderId={},skuCode={}", orderId, skuCode);
                    return;
                }
                // 5.释放商品库存
                releaseProductStockProcessor.doRelease(skuCode, saleQuantity, productStockLog);
            } finally {
                // 释放分布式锁
                redisLock.unlock(lockKey);
            }
        }
    }

    @Override
    public void addProductStock(AddProductStockParam param) {
        log.info("新增商品库存：request={}", JSONObject.toJSONString(param));
        // 1、校验入参
        checkAddProductStockParam(param);

        // 2、查询商品库存
        ProductStockDO productStock = productStockDAO.getBySkuCode(param.getSkuCode());
        ParamCheckUtil.checkObjectNull(productStock,InventoryErrorCodeEnum.PRODUCT_SKU_STOCK_EXISTED_ERROR);

        // 3、添加redis锁，防并发
        String lockKey = RedisLockKeyConstants.ADD_PRODUCT_STOCK_KEY + param.getSkuCode();
        Boolean locked = redisLock.tryLock(lockKey);
        if(!locked) {
            throw new InventoryBizException(InventoryErrorCodeEnum.ADD_PRODUCT_SKU_STOCK_ERROR);
        }
        try {
            // 4、执行添加商品库存逻辑
            addProductStockProcessor.doAdd(param);
        } finally {
            // 5、解锁
            redisLock.unlock(lockKey);
        }
    }

    @Override
    public void modifyProductStock(ModifyProductStockParam param) {
        log.info("调整商品库存：request={}", JSONObject.toJSONString(param));
        //1、校验入参
        checkModifyProductStockParam(param);

        // 2、查询商品库存
        ProductStockDO productStock = productStockDAO.getBySkuCode(param.getSkuCode());
        ParamCheckUtil.checkObjectNonNull(productStock,InventoryErrorCodeEnum.PRODUCT_SKU_STOCK_NOT_FOUND_ERROR);

        // 3、校验库存被扣减后是否小于0
        Long stockIncremental = param.getStockIncremental();
        Long saleStockQuantity = productStock.getSaleStockQuantity();
        if(saleStockQuantity + stockIncremental < 0) {
            throw new InventoryBizException(InventoryErrorCodeEnum.SALE_STOCK_QUANTITY_CANNOT_BE_NEGATIVE_NUMBER);
        }

        // 4、加分布式锁，保证mysql和redis数据的一致性
        String lockKey = RedisLockKeyConstants.MODIFY_PRODUCT_STOCK_KEY + param.getSkuCode();
        Boolean locked = redisLock.tryLock(lockKey);
        if(!locked) {
            throw new InventoryBizException(InventoryErrorCodeEnum.MODIFY_PRODUCT_SKU_STOCK_ERROR);
        }

        try {
            modifyProductStockProcessor.doModify(productStock,stockIncremental);
        } finally {
            redisLock.unlock(lockKey);
        }
    }


    @Override
    public void syncStockToCache(SyncStockToCacheParam param) {
        // 1、校验参数
        ParamCheckUtil.checkStringNonEmpty(param.getSkuCode(),InventoryErrorCodeEnum.SKU_CODE_IS_EMPTY);

        // 2、同步
        syncStockToCacheProcessor.doSync(param.getSkuCode());
    }

    private void checkModifyProductStockParam(ModifyProductStockParam param) {
        ParamCheckUtil.checkStringNonEmpty(param.getSkuCode(),InventoryErrorCodeEnum.SKU_CODE_IS_EMPTY);
        ParamCheckUtil.checkObjectNonNull(param.getStockIncremental(),InventoryErrorCodeEnum.SALE_STOCK_INCREMENTAL_QUANTITY_IS_EMPTY);
        if(0L == param.getStockIncremental()) {
            throw new InventoryBizException(InventoryErrorCodeEnum.SALE_STOCK_INCREMENTAL_QUANTITY_CANNOT_BE_ZERO);
        }
    }

    private void checkAddProductStockParam(AddProductStockParam param) {
        ParamCheckUtil.checkStringNonEmpty(param.getSkuCode(),InventoryErrorCodeEnum.SKU_CODE_IS_EMPTY);
        ParamCheckUtil.checkObjectNonNull(param.getSaleStockQuantity(),InventoryErrorCodeEnum.SALE_STOCK_QUANTITY_IS_EMPTY);
        ParamCheckUtil.checkLongMin(param.getSaleStockQuantity(),0L,InventoryErrorCodeEnum.SALE_STOCK_QUANTITY_CANNOT_BE_NEGATIVE_NUMBER);
    }


    /**
     * 检查释放商品库存入参
     * @author xuyou
     * @date: 2022/7/1 13:52
     * @param: param
     * @return: void
     */
    private void checkReleaseProductStockParam(ReleaseProductStockParam param) {
        ParamCheckUtil.checkStringNonEmpty(param.getOrderId());
        ParamCheckUtil.checkCollectionNonEmpty(param.getOrderItemParamList());
    }


    /**
     * 检查锁定商品库存入参
     * @author xuyou
     * 2022/6/4 0004 22:37
     * @param param
    */
    private void checkLockProductStockParam(DeductProductParam param) {
        String orderId = param.getOrderId();
        ParamCheckUtil.checkStringNonEmpty(orderId);
        List<DeductProductParam.OrderItemParam> orderItemParamList =
                param.getOrderItemParamList();
        ParamCheckUtil.checkCollectionNonEmpty(orderItemParamList);
    }
}
